package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.SystemConstants;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 聪哥
 * @since 2024-09-11
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 注入自定义封装的Redis缓存工具
     */
    @Resource
    private CacheClient cacheClient;

    /**
     * 开启线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 根据id查询店铺（添加Redis缓存版 + 解决缓存穿透【缓存空对象】+ 互斥锁实现方案）
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //1. 根据id到Redis中查询用户信息
        //2. Redis命中 ----------------> 返回商户信息 -------> 结束
        //2.改：Redis命中 ----------------> 判断是否为空对象-------> 结束
        //                                        | 非空
        //                                        ------> 返回商户信息 -------> 结束
        //3. Redis未命中  -----> 查询数据库
        //3.改：Redis未命中 -------> 尝试获取互斥锁 ------> 获取成功 -----> 查询数据库 -------> 将数据库结果写入Redis -------> 释放锁 ------> 返回商户信息 -------> 结束
        //                                     | 获取失败
        //                                     ------> 休眠一段时间后重试
        //4. 查询数据库未命中 ----------------> 返回错误信息 ------> 结束
        //4.改： 查询数据库未命中----------> 将空对象写入Redis  ------> 结束
        //5. 数据库命中 ---------------> 将数据写入Redis ------> 返回商户信息 -------> 结束
        // -------------------------------------------上述思路将被分装成两个方法分别解决 缓存穿透 和 缓存击穿 问题------------------------------------------------------------------------------------------------ //

        // 解决缓存穿透问题
//        Shop shop = queryWithPassThrough(id);

        // 基于互斥锁解决缓存击穿问题
        Shop shop = queryWithMutex(id);

        // 基于逻辑过期解决缓存击穿问题 【需要提前准备好Redis缓存数据】
//        Shop shop = queryWithLogicalExpire(id);


        // 基于自定义封装的Redis缓存工具解决缓存穿透问题
//        Shop shop = cacheClient.queryWithPassThrough(
//                CACHE_SHOP_KEY,
//                id,
//                Shop.class,
//                this::getById,
//                CACHE_SHOP_TTL,
//                TimeUnit.SECONDS);

        // 基于自定义封装的Redis缓存工具解决缓存击穿问题【逻辑过期时间】
//        Shop shop = cacheClient.queryWithLogicalExpire(
//                CACHE_SHOP_KEY,
//                id,
//                Shop.class,
//                this::getById,
//                CACHE_SHOP_TTL,
//                TimeUnit.MINUTES);

        if (shop == null){
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    /**
     * 更新店铺信息
     * @param shop
     * @return
     */
    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id == null){
            return Result.fail("店铺id不能为空");
        }
        // 1. 更新数据库
        updateById(shop);
        // 2. 删除缓存
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shop.getId());
        // 3. 返回结果
        return Result.ok();
    }

    /**
     * 根据类型分页分类查询店铺信息
     * @param typeId
     * @param current
     * @param x
     * @param y
     * @return
     */
    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //1.判断 是否需要根据坐标查询
        if(x==null || y == null){
            // 不需要坐标查，按数据库查
            // 根据类型分页查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }

        //2. 计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        //3. 查询redis，按照类型、页码、距离升序：结果：shopId 、 distance
        String key = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(
                        key,
                        GeoReference.fromCoordinate(x,y),
                        new Distance(Shop_FEO_DISTANCE),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
                                .includeDistance()
                                .limit(end)
                );

        //4. 解析出id
        if(results == null){
            return Result.ok(Collections.emptyList());
        }
        // 解析
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();

        if(list.size() <= from){
            return Result.ok(Collections.emptyList());
        }

        //4.1 截取 from 到 end 的部分
//        list.subList(from,end);
        // 定义一个集合，保存店铺id和距离的关系
        List<Long> ids = new ArrayList<>(list.size());
        Map<String,Distance> distanceMap = new HashMap<>(list.size());

        list.stream().skip(from).forEach(result -> {
            // 4.2 获取店铺id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            // 4.3 获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr,distance);

        });
        //5. 查询店铺
        List<Shop> shops = query().in("id",ids)
                .last("ORDER BY FIELD(id," + StrUtil.join(",",ids) + ")")
                .list();

        // 匹配店铺距离
        for(Shop shop : shops) {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }

        //6. 返回结果
        return Result.ok(shops);
    }


    /**
     * 获取互斥锁  setNx  ----- setIfAbsent
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",RedisConstants.LOCK_SHOP_TTL,TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放互斥锁
     * @param key
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

    /**
     * 解决缓存穿透问题保存代码
     * @param id
     * @return
     */
    private Shop queryWithPassThrough(Long id){
        String stopJson =  stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        // 判断Redis中是否存在数据
        if(StrUtil.isNotBlank(stopJson)){
            // 存在直接返回
            Shop shop = JSONUtil.toBean(stopJson, Shop.class); //将JSON字符串转换为对象
//            return Result.ok(shop);
            return shop;
        }
        // 判断Redis中是否存在空对象
        if  (RedisConstants.CACHE_PENETRATION_NULL_VALUE.equals(stopJson)){
//            return Result.fail("店铺信息不存在");
            return null;
        }

//        query().eq("shop_id",id);
        // 查询数据库
        Shop shop = getById(id);
        // 查询数据库不存在
        if(shop == null){
            // 将空对象写入Redis
            stringRedisTemplate.opsForValue().set(
                    RedisConstants.CACHE_SHOP_KEY + id,
                    RedisConstants.CACHE_PENETRATION_NULL_VALUE,
                    RedisConstants.CACHE_NULL_TTL,
                    TimeUnit.MINUTES
            );
//            return Result.fail("店铺不存在");
            return null;
        }

        // 将数据写入Redis + 设置过期时间
        stringRedisTemplate.opsForValue().set(
                RedisConstants.CACHE_SHOP_KEY + id,
                JSONUtil.toJsonStr(shop),
                RedisConstants.CACHE_SHOP_TTL,
                TimeUnit.MINUTES
        );
//        return Result.ok(shop);
        return shop;
    }


    /**
     * 基于互斥锁解决缓存击穿问题保存代码
     * @param id
     * @return
     */
    private Shop queryWithMutex(Long id){
        String stopJson =  stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        // 判断Redis中是否存在数据
        if(StrUtil.isNotBlank(stopJson)){
            // 存在直接返回
            Shop shop = JSONUtil.toBean(stopJson, Shop.class); //将JSON字符串转换为对象
//            return Result.ok(shop);
            return shop;
        }
        // 判断Redis中是否存在空对象
        if  (RedisConstants.CACHE_PENETRATION_NULL_VALUE.equals(stopJson)){
//            return Result.fail("店铺信息不存在");
            return null;
        }

        // 4. 实现缓存重建
        //4.1 尝试获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            //4.2 获取互斥锁失败 休眠一段时间后 重新查询Redis(自旋)
            if(!isLock){
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //4.3 获取互斥锁成功 查询数据库 将商铺信息写入Redis
//        query().eq("shop_id",id);
            // 查询数据库
            shop = getById(id);

            // 模拟重建的延迟情况
            Thread.sleep(100);

            // 查询数据库不存在
            if(shop == null){
                // 将空对象写入Redis
                stringRedisTemplate.opsForValue().set(
                        RedisConstants.CACHE_SHOP_KEY + id,
                        RedisConstants.CACHE_PENETRATION_NULL_VALUE,
                        RedisConstants.CACHE_NULL_TTL,
                        TimeUnit.MINUTES
                );
    //            return Result.fail("店铺不存在");
                return null;
            }

            // 将数据写入Redis + 设置过期时间
            stringRedisTemplate.opsForValue().set(
                    RedisConstants.CACHE_SHOP_KEY + id,
                    JSONUtil.toJsonStr(shop),
                    RedisConstants.CACHE_SHOP_TTL,
                    TimeUnit.MINUTES
            );
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //4.4 释放互斥锁
            unLock(lockKey);
        }
        //4.5 返回商铺信息
//        return Result.ok(shop);
        return shop;
    }


    /**
     * 创建RedisData对象 【原Shop + 逻辑过期时间字段】
     * @param id
     * @param expireTime
     */
    public void saveShop2Redis(Long id, Long expireTime) throws InterruptedException {
        // 1.查询店铺数据
        Shop shop = getById(id);
        // 模拟休息情况
        Thread.sleep(100);
        // 2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
        // 3.写入Redis
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }


    /**
     * 基于逻辑过期解决缓存击穿问题 保存代码、
     * 为什么不用判断缓存穿透问题，因为一定有Key，如果没有Key必然不对，不需要让它继续访问数据库了，直接打回就可
     * @param id
     * @return
     */
    private Shop queryWithLogicalExpire(Long id){

        String stopJson =  stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        //1. 判断Redis中是否存在数据
        if(StrUtil.isBlank(stopJson)){
            //1.1  不存在直接结束
            return null;
        }
        //2. 命中了，判断缓存是否过期
        //2.1 将JSON字符串反序列为对象
        RedisData redisData = JSONUtil.toBean(stopJson,RedisData.class);
        JSONObject data =(JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data,Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        if(expireTime.isAfter(LocalDateTime.now())){ // 未过期
            // 直接返回
            return shop;

        }
        // 3. 已过期，需要缓存重建
        // 4. 尝试获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if(isLock){
            //5. 获取互斥锁成功，开启独立线程，查询数据库，重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() ->{
                try {
                    //  重建缓存
                    this.saveShop2Redis(id, 20L);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 6. 释放互斥锁
                    unLock(lockKey);
                }
            });
        }
        return shop;
    }

}
